/*
 * To change this template, choose Tools | Templates
 * and open the template in the editor.
 */

package net.worlds.net;

import java.awt.Color;
import java.io.ByteArrayOutputStream;
import java.io.DataInputStream;
import java.io.DataOutputStream;
import java.io.IOException;
import java.net.Socket;
import java.nio.ByteBuffer;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;
import java.util.zip.GZIPOutputStream;
import net.worlds.WorldsException;
import net.worlds.Main;
import net.worlds.ObjectList;
import net.worlds.Util;
import net.worlds.World;
import net.worlds.object.Box;
import net.worlds.object.Camera;
import net.worlds.object.Connection;
import net.worlds.object.GameObject;
import net.worlds.object.NetworkServer;
import net.worlds.object.Part;
import net.worlds.object.Player;
import net.worlds.object.Sound;
import net.worlds.object.Tool;
import net.worlds.object.UIComponent;
import net.worlds.object.VelocityController;
import net.worlds.ui.UI;
import org.jbox2d.common.Vec2D;
import org.jbox2d.dynamics.Body;


public class RemotePlayer implements Runnable {
    private final List<Runnable> tasks = Collections.synchronizedList(new ArrayList<Runnable>(1024));
    private Socket socket;
    private Connection connection;
    private String ip;
    private String name;
    private Thread thread;
    private Thread sendThread;
    private Thread pingThread;
    private Thread positionThread;
    private Thread velocityThread;
    private Thread playerUpdateThread;
    private DataInputStream in;
    private DataOutputStream out;
    private boolean running = false;
    private boolean disconnected = false;
    public boolean levelSent = false;
    private final ByteBuffer outputbuf = ByteBuffer.allocate(1048576);
    private final ByteBuffer inputbuf = ByteBuffer.allocate(1048576);
    private int currentSize = 0;
    private long lastPingTime = System.currentTimeMillis();
    private long lastPingSendTime;
    private long timeout = 45000;
    private final RemotePlayer instance = this;
    public RemotePlayer(Socket s)
    {
        try {
            socket = s;
            ip = s.getInetAddress().toString()+":"+s.getPort();
            Main.log("Connection from " + s.getInetAddress());
            in = new DataInputStream(s.getInputStream());
            out = new DataOutputStream(s.getOutputStream());
            running = true;
            thread = new Thread(this);
            thread.setName("Player");
            thread.start();
            sendThread = new Thread(new DataSender());
            sendThread.setName("DataSender");
            sendThread.start();
            pingThread = new Thread(new PingThread());
            pingThread.setName("Ping");
            pingThread.start();
            if(Main.isBuildServer())
            {
                playerUpdateThread = new Thread(new PlayerUpdateThread());
                playerUpdateThread.start();
            }
            connection = (Connection) Main.getGame().getMainWorld().createObject("Connection");
            connection.setProp("Name", ip, true);
            connection.setProp("IP", ip, true);
            connection.setProp("Parent", Main.getGame().getMainWorld().getService("NetworkServer"), true);
            connection.setClient(this);
        } catch (Exception ex) {
            Main.log("Error handling "+ip+": "+ex);
            ex.printStackTrace();
        }
    }
    private class DataSender implements Runnable
    {
        public void run()
        {
            while(true)
            {
                try
                {
                    synchronized(tasks)
                    {
                        Iterator<Runnable> itr = tasks.iterator();
                        while(itr.hasNext())
                        {
                            itr.next().run();
                            itr.remove();
                        }
                        try {
                            tasks.wait();
                        } catch (InterruptedException ex) {
                            ex.printStackTrace();
                         }
                    }
                }
                catch(Exception ex)
                {
                    ex.printStackTrace();
                }
            }
        }
    }
    private void addTask(Runnable r)
    {
        synchronized(tasks)
        {
            tasks.add(r);
            tasks.notify();
        }
    }
    public Connection getConnection()
    {
        return connection;
    }
    public String getName()
    {
        return name;
    }
    public void disconnect()
    {
        if(disconnected)
            return;
        disconnected = true;
        Main.getServer().removePlayer(instance);
        Main.log(name+" left the game");
        Main.getServer().sendPlayerRemove(instance);
        running = false;
        try {
            List<GameObject> children = Main.getGame().getMainWorld().getService("NetworkServer").getChildren();
            synchronized(children)
            {
                for(final GameObject c : children)
                {
                    if(c instanceof Connection && ((Connection)c).getStringProp("IP").equals(ip))
                        Main.runInMainThread(new Runnable()
                        {
                            public void run() {
                            try {
                                c.remove();
                            } catch (WorldsException ex) {
                                throw new RuntimeException(ex);
                            }
                            }
                        });
                }
            }
            if(!socket.isClosed())
                socket.close();
            if(positionThread != null)
                positionThread.stop();
            if(velocityThread != null)
                velocityThread.stop();
            if(playerUpdateThread != null && Main.isBuildServer())
                playerUpdateThread.stop();
            if(pingThread != null)
                pingThread.stop();
        } catch (Exception ex) {
            ex.printStackTrace();
        }
    }
    public void run() {
        try
        {
            while(true)
            {
                if(!running || in == null || out == null)
                    break;
                int size = in.readShort();
                currentSize = size;
                int cmd = in.read();
                processPacket(size, cmd, in);
                if(System.currentTimeMillis() - lastPingTime > timeout)
                    disconnect();
            }
        }
        catch(IOException ex)
        {
            disconnect();
        }
    }
    private void writeData(ByteBuffer bb)
    {
        bb.flip();
        if(disconnected)
            return;
        try {
            out.writeShort(bb.limit());
            out.write(bb.array(), 0, bb.limit());
            out.flush();
        } catch (IOException ex) {
            Main.log("Error writing data: "+ex);
            disconnect();
            ex.printStackTrace();
        }
    }
    private void processPacket(int size, int cmd, DataInputStream in) throws IOException
    {
        if(currentSize == 0)
            return;
        inputbuf.clear();
        for(int i = 0; i < currentSize - 1; i++)
        {
            inputbuf.put((byte) in.read());
        }
        inputbuf.flip();
        switch(cmd)
        {
            case 0:
                lastPingTime = System.currentTimeMillis();
            break;
            case 1:
                final String s = getString();
                if(Main.debug)
                    Main.log(ip+" logged in as "+s);
                name = s;
                sendServerInfo();
                sendGame();
                sendLevelFinish();
                Main.getServer().broadcastChat(new Color(85, 101, 131), s+" joined the game");
            break;
            case 5:
                final String type = getString();
                final int requestID = inputbuf.getShort();
                final String worldName = getString();
                Main.runInMainThread(new Runnable()
                {
                    public void run()
                    {
                        if(Main.debug)
                            System.err.println("Received Create: "+type);
                        int newID = Main.getGame().getFreeID();
                        GameObject obj = Main.getGame().getWorld(worldName).createObject(type, requestID, newID, instance);
                        sendIDChange(requestID, newID);
                        if(obj instanceof Part)
                            sendObjectFinish((Part)obj);
                    }
                });
            break;
            case 6:
                final int id6 = inputbuf.getShort();
                final boolean isClientID = getBoolean();
                final String key = getString();
                final boolean fireEvent = getBoolean();
                final int propType = inputbuf.get();
                int objID = -1;
                Object value = null;
                if(propType == 0)
                    value = inputbuf.getInt();
                else if(propType == 1)
                    value = inputbuf.getFloat();
                else if(propType == 2)
                    value = getBoolean();
                else if(propType == 3)
                    value = getString();
                else if(propType == 4)
                    objID = inputbuf.getShort();
                else if(propType == 5)
                    value = getVec2D();
                else if(propType == 6)
                    value = getColor();
                else
                    throw new RuntimeException("Unexpected property type: "+propType);
                final Object finalValue = value;
                final int finalID = objID;
                Main.runInMainThread(new Runnable()
                {
                    public void run()
                    {
                        if(Main.debug)
                            System.err.println("Received Property: "+key+" "+finalValue);
                        GameObject obj6 = null;
                        if(isClientID)
                        {
                            obj6 = Main.getGame().getByOldID(id6);
                        }
                        else
                            obj6 = Main.getGame().getByID(id6);
                        if(obj6 == null)
                            return;
                        try {
                            if(finalValue == null)
                            {
                                if(finalID == Short.MIN_VALUE)
                                    obj6.setProp(key, null, fireEvent, instance);
                                else if(finalID >= 0)
                                    obj6.setProp(key, Main.getGame().getByID(finalID), fireEvent, instance);
                                else
                                    obj6.setProp(key, Main.getGame().getByOldID(-finalID), fireEvent, instance);
                            }
                            else
                                obj6.setProp(key, finalValue, fireEvent, instance);
                        } catch (WorldsException ex) {
                            throw new RuntimeException(ex.getError());
                        }
                    }
                });
            break;
            case 7:
                final int id7 = inputbuf.getShort();
                final Vec2D pos = getVec2D();
                final double rotation = inputbuf.getFloat();
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        GameObject part = getObject(id7);
                        if(part == null || !(part instanceof GameObject))
                            return;
                        ((Part)part).getBody().setXForm(pos, (float) rotation);
                    }
                });
            break;
            case 8:
                final int id8 = inputbuf.getShort();
                final Vec2D vel = getVec2D();
                final double rvel = inputbuf.getFloat();
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        GameObject part8 = getObject(id8);
                        if(part8 == null || !(part8 instanceof Part))
                            return;
                        ((Part)part8).getBody().setLinearVelocity(vel);
                        ((Part)part8).getBody().setAngularVelocity((float) rvel);
                    }
                });
            break;
            case 9:
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        levelSent = true;
                        String script = Util.getTextFromResource("/createServerCharacter.lua");
                        script = script.replace("$ADDCHARACTER", ""+!Main.getServer().isBuildServer);
                        script = script.replace("$NAME", name);
                        script = script.replace("$IP", ip);
                        script = script.replace("$CONNECTION", ""+connection.getID());
                        Main.getScriptContext().createScriptThread(null, "CreateServerCharacter", script).start();
                        positionThread = new Thread(new PositionUpdateThread());
                        positionThread.setName("Position");
                        positionThread.start();
                        velocityThread = new Thread(new VelocityUpdateThread());
                        velocityThread.setName("Velocity");
                        velocityThread.start();
                    }
                });
            break;
            case 11:
                Vec2D characterPos = getVec2D();
                Vec2D characterVel = getVec2D();
                connection.moveCharacter(characterPos, characterVel);
            break;
            case 12:
            break;
            case 13:
                int status = inputbuf.get();
                if(status == 0)
                    Main.getGame().stop(true);
                else
                    Main.getGame().play(true);
            break;
            case 14:
                final Vec2D vec = getVec2D();
                int jointType = inputbuf.get();
                final int oldMode = UI.mouseMode;
                if(jointType == 0)
                    UI.mouseMode = UI.MODE_WELD;
                else if(jointType == 1)
                    UI.mouseMode = UI.MODE_HINGE;
                    Main.runInMainThread(new Runnable()
                    {
                        public void run() {
                            try {
                                UI.makeJoint(vec);
                                UI.mouseMode = oldMode;
                            } catch (WorldsException ex) {
                                ex.printStackTrace();
                            }
                        }
                    });
            break;
            case 15:
                final int inUseID = inputbuf.getShort();
                final boolean inUseStatus = getBoolean();
                Main.runInMainThread(new Runnable()
                {

                    public void run() {
                        GameObject inUseObj = getObject(inUseID);
                        if(inUseObj == null)
                            return;
                        inUseObj.setInUse(inUseStatus);
                    }
                });
            break;
            case 17:
                Vec2D playerpos = getVec2D();
                if(Main.getServer().cursors.get(this) == null)
                {
                    PlayerCursor c = new PlayerCursor();
                    if(getConnection() != null && getConnection().getPlayer() != null)
                    {
                        c.player = (Player) getConnection().getPlayer();
                        c.client = this;
                        c.pos = playerpos;
                        if(c.player != null)
                            Main.getServer().cursors.put(this, c);
                    }
                }
                else
                {
                    PlayerCursor c = Main.getServer().cursors.get(this);
                    c.pos = playerpos;
                }
            break;
            case 18:
                String source = getString();
                Main.getScriptContext().createScriptThread(null, "Script_Command", source).start();
            break;
            case 19:
                final int oldID = inputbuf.getShort();
                final int newID = inputbuf.getShort();
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        GameObject changeObj = Main.getGame().getByID(oldID);
                        if(changeObj != null)
                        {
                            changeObj.setID(newID, false);
                        }
                        else
                            Main.log("obj == null");
                    }
                });
            break;
            case 21:
                final String msg = getString();
                Main.getServer().broadcastChat(instance, msg);
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        if(connection != null && connection.getPlayer() != null)
                            connection.getPlayer().getEvent("Chatted").fire(new Object[]{msg});
                    }
                });
            break;
            case 22:
                final int id = inputbuf.getShort();
                final int action = inputbuf.get();
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        GameObject obj = getObject(id);
                        try
                        {
                            if(obj instanceof Part)
                            {
                                if(action == 0)
                                    UI.moveToBack((Part)obj);
                                else
                                    UI.moveToFront((Part)obj);
                            }
                        }
                        catch(WorldsException ex)
                        {
                            ex.printStackTrace();
                        }
                    }
                });
            break;
            case 23:
                final short toolActiveID = inputbuf.getShort();
                final boolean toolActive = getBoolean();
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        Tool t = (Tool) Main.getGame().getByID(toolActiveID);
                        if(t != null)
                        {
                            if(toolActive)
                                t.selected();
                            else
                                t.deselected();
                        }
                    }
                });
            break;
            case 24:
                final short toolMouseID = inputbuf.getShort();
                final int toolMouseAction = inputbuf.get();
                final int toolMouseButton = inputbuf.get();
                final Vec2D toolMousePosition = getVec2D();
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        Tool t = (Tool) Main.getGame().getByID(toolMouseID);
                        if(t != null)
                        {
                            switch(toolMouseAction)
                            {
                                case 0:
                                    t.mouseDown(toolMouseButton, toolMousePosition);
                                break;
                                case 1:
                                    t.mouseDragged(toolMouseButton, toolMousePosition);
                                break;
                                case 2:
                                    t.mouseUp(toolMouseButton, toolMousePosition);
                                break;
                            }
                        }
                    }
                });
            break;
            case 25:
                final short toolKeyID = inputbuf.getShort();
                final int toolKeyAction = inputbuf.get();
                final String toolKey = getString();
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        Tool t = (Tool) Main.getGame().getByID(toolKeyID);
                        if(t != null)
                        {
                            if(toolKeyAction == 0)
                                t.keyDown(toolKey);
                            else
                                t.keyUp(toolKey);
                        }
                    }
                });
            break;
            case 26:
                final short clickID = inputbuf.getShort();
                final int btn = inputbuf.get();
                if(connection != null && connection.getPlayer() != null && levelSent)
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        Part p = (Part) Main.getGame().getByID(clickID);
                        if(p != null)
                            p.getEvent("Clicked").fire(new Object[]{connection.getPlayer(), btn});
                    }
                });
            break;
            case 27:
                final int toolID = inputbuf.getShort();
                try {
                    if (connection != null && connection.getPlayer() != null && levelSent && connection.getPlayer().getObjectProp("Character") != null) {
                        Main.runInMainThread(new Runnable() {

                            public void run() {
                                try {
                                    net.worlds.object.Character character = ((net.worlds.object.Character) connection.getPlayer().getObjectProp("Character"));
                                    Tool tool = (Tool) Main.getGame().getByID(toolID);
                                    if(toolID == -1)
                                    {
                                        character.net_selectedTool = null;
                                        character.net_armRotation = false;
                                    }
                                    else
                                    {
                                        character.net_selectedTool = tool;
                                        character.net_armRotation = true;
                                    }
                                    Main.getServer().sendCharacterSelectedTool(character, tool);
                                } catch (WorldsException ex) {
                                    ex.printStackTrace();
                                }
                            }
                        });
                    }
                } catch (WorldsException ex) {
                    ex.printStackTrace();
                }
            break;
            case 28:
                final double armRotation = (double) inputbuf.getFloat();
                try {
                    if (connection != null && connection.getPlayer() != null && levelSent && connection.getPlayer().getObjectProp("Character") != null) {
                        Main.runInMainThread(new Runnable() {
                            public void run() {
                                try {
                                    net.worlds.object.Character character = ((net.worlds.object.Character) connection.getPlayer().getObjectProp("Character"));
                                    character.net_armAngle = armRotation;
                                } catch (WorldsException ex) {
                                    ex.printStackTrace();
                                }
                            }
                        });
                    }
                } catch (WorldsException ex) {
                    ex.printStackTrace();
                }
            break;
            case 30:
                final int gbtn = inputbuf.get();
                final short gclickID = inputbuf.getShort();
                final int x = inputbuf.getShort();
                final int y = inputbuf.getShort();
                if(connection != null && connection.getPlayer() != null && levelSent)
                Main.runInMainThread(new Runnable()
                {
                    public void run() {
                        UIComponent p = (UIComponent) Main.getGame().getByID(gclickID);
                        if(p != null)
                            p.getEvent("MouseDown").fire(new Object[]{connection.getPlayer(), gbtn, new Vec2D(x, y)});
                    }
                });
            break;
            default:
                throw new RuntimeException("Invalid command: "+cmd);
        }
    }
    public void sendPing()
    {
        addTask(new Runnable()
        {
            public void run() {
                
                {
                    outputbuf.clear();
                    outputbuf.put((byte) 0);
                    outputbuf.put((byte) 0);
                    writeData(outputbuf);
                }
            }
        });
    }
    public void sendServerInfo()
    {
        addTask(new Runnable()
        {
            public void run() {
                
                {
                    outputbuf.clear();
                    outputbuf.put((byte) 1);
                    writeBoolean(Main.getGame().isPlaying);
                    writeData(outputbuf);
                }
            }
        });
    }
    public void sendGame()
    {
        addTask(new Runnable()
        {
            public void run() {
                try {
                    String g = Main.getGame().serializeToString();
                    ByteBuffer bb = ByteBuffer.wrap(g.getBytes());
                    byte[] uncompressedlevelBytes = bb.array();
                    ByteArrayOutputStream bOut = new ByteArrayOutputStream();
                    GZIPOutputStream gzOut = new GZIPOutputStream(bOut);
                    gzOut.write(uncompressedlevelBytes);
                    gzOut.close();
                    byte[] levelBytes = bOut.toByteArray();
                    System.err.println(levelBytes.length);

                    bOut.close();

                    int length = levelBytes.length;
                    sendGameStart(length);
                    int offset = 0;
                    byte[] output = new byte[1020];
                    while(offset < length)
                    {
                        Arrays.fill(output, (byte) 2);
                        int chunkLength = 1020;
                        if(offset + 1020 > length)
                            chunkLength = length - offset;
                        int j = 0;
                        for(int i = offset; i < offset + chunkLength; i++)
                        {
                            output[j] = levelBytes[i];
                            j++;
                        }
                        System.err.println(chunkLength);
                        sendGameChunk(chunkLength, output);
                        offset += 1020;
                    }
                    sendGameFinish(g.length());
                } catch (Exception ex) {
                    ex.printStackTrace();
                }
            }
        });
    }
    private void sendGameStart(final int len)
    {
        outputbuf.clear();
        outputbuf.put((byte)101);
        outputbuf.putInt(len);
        writeData(outputbuf);
    }
    private void sendGameChunk(final int size, final byte[] data)
    {
        outputbuf.clear();
        outputbuf.put((byte)102);
        outputbuf.putShort((short)size);
        outputbuf.put(data);
        writeData(outputbuf);
    }
    private void sendGameFinish(final int size)
    {
        outputbuf.clear();
        outputbuf.put((byte)103);
        outputbuf.putInt(size);
        writeData(outputbuf);
    }
    public void sendLevelFinish()
    {
        addTask(new Runnable()
        {
            public void run() {
                
                {
                    outputbuf.clear();
                    outputbuf.put((byte) 4);
                    outputbuf.put((byte) 0);
                    writeData(outputbuf);
                }
            }
        });
    }
    public void sendObjectRecursive(final GameObject obj)
    {
        addTask(new Runnable()
        {
            public void run() {
                _sendObjectRecursive(obj);
            }
        });
    }
    private void _sendObjectRecursive(final GameObject obj)
    {
        if(obj.ownerPlayer == this || !obj.isSendable)
            return;
        List<GameObject> children = obj.getChildren();
        synchronized(children)
        {
            for(GameObject c : children)
            {
                if(!(c instanceof NetworkServer))
                {
                    c.isSendable = true;
                    _sendObject(c);
                    _sendObjectRecursive(c);
                }
            }
        }
    }
    public void sendObject(final GameObject obj)
    {
        Runnable r = new Runnable()
        {
            public void run() {
                _sendObject(obj);
            }
        };
        if(Thread.currentThread() == sendThread)
            r.run();
        else
            addTask(r);
    }
    private void _sendObject(final GameObject obj)
    {
        if(obj.ownerPlayer == this || !obj.isSendable || Util.isLocal(obj.getType()))
            return;
            
            {
                sendObjectCreate(obj);
                try {
                    for(String k : obj.getAllProperties().keySet())
                    {
                            boolean fireEvent = !k.equals("Parent");
                            if(obj.getProp(k) != null && (k.equals("Parent") || ObjectList.getPropDef(obj.getClass().getSimpleName(), k).defaultValue != obj.getProp(k)))
                                sendProp(obj, k, obj.getProp(k), fireEvent);
                    }
                    if(obj.getParent() != null)
                    sendProp(obj, "Parent", obj.getParent(), true);
                } catch (WorldsException ex) {
                    ex.printStackTrace();
                }
            }
    }
    public void sendObjectCreate(final GameObject obj)
    {
        if(obj.ownerPlayer == this || !obj.isSendable || Util.isLocal(obj.getType()))
            return;
        Runnable r = new Runnable()
        {
                public void run() {
                {
                    if(Main.debug)
                        System.err.println("Replicated Create: "+obj.getID()+" "+obj);
                    outputbuf.clear();
                    outputbuf.put((byte) 5);
                    writeString(obj.getType());
                    outputbuf.putShort((short) obj.getID());
                    writeString(obj.world.name);
                    writeData(outputbuf);
                }
            }
        };
        if(Thread.currentThread() == sendThread)
            r.run();
        else
            addTask(r);
    }
    public void sendProp(final GameObject obj, final String name, final Object value, final boolean fireEvent)
    {
        try {
            if(obj.ownerPlayer == this || !obj.isSendable || Util.isLocal(obj.getType()) || (obj instanceof VelocityController && name.equals("Velocity")) || (obj instanceof Camera && !(obj.getParent() instanceof Player)))
            {
                //if(Main.debug)
                //    System.err.println("Didn't Replicate Property: "+obj.getID()+" "+obj+" "+name+" "+value);
                return;
            }
        } catch (WorldsException ex) {
            ex.printStackTrace();
        }
        if(name.equals("Type") || (name.equals("Name") && value.equals(obj.getType())))
            return;
        Runnable r = new Runnable()
        {
            public void run() {

                {
                    if(Main.debug)
                        System.err.println("Replicated Property: "+obj.getID()+" "+obj+" "+name+" "+value);
                    outputbuf.clear();
                    outputbuf.put((byte) 6);
                    outputbuf.putShort((short) obj.getID());
                    writeString(name);
                    writeBoolean(fireEvent);
                    if(value instanceof Integer)
                    {
                        outputbuf.put((byte) 0);
                        outputbuf.putInt((Integer)value);
                    }
                    else if(value instanceof Float)
                    {
                        outputbuf.put((byte) 1);
                        outputbuf.putFloat((Float) value);
                    }
                    else if(value instanceof Boolean)
                    {
                        outputbuf.put((byte) 2);
                        writeBoolean((Boolean) value);
                    }
                    else if(value instanceof String)
                    {
                        outputbuf.put((byte) 3);
                        writeString((String) value);
                    }
                    else if(value instanceof GameObject)
                    {
                        outputbuf.put((byte) 4);
                        writeObject((GameObject) value);
                    }
                    else if(value instanceof Vec2D)
                    {
                        outputbuf.put((byte) 5);
                        writeVec2D((Vec2D) value);
                    }
                    else if(value instanceof Color)
                    {
                        outputbuf.put((byte) 6);
                        writeColor((Color) value);
                    }
                    else if(value == null && ObjectList.getPropDef(obj.getClass().getSimpleName(), name).type == GameObject.class || name.equals("Parent"))
                    {
                        outputbuf.put((byte) 4);
                        writeObject(null);
                    }
                    else
                        throw new RuntimeException("Unexpected property type: "+value.getClass());
                    writeData(outputbuf);
                }
            }
        };
        if(Thread.currentThread() == sendThread)
            r.run();
        else
            addTask(r);
    }
    public void sendObjectPositions()
    {
        addTask(new Runnable()
        {
            public void run() {
                Player player = connection.getPlayer();
                World world = null;
                try {
                    if (player != null && player.getProp("Character") != null && player.getProp("Character") instanceof net.worlds.object.Character) {
                        world = player.getObjectProp("Character").world;
                    }
                } catch (WorldsException ex) {
                    ex.printStackTrace();
                }
                if(world != null)
                {
                    outputbuf.clear();
                    outputbuf.put((byte) 7);
                    writeString(world.name);
                    for(Body b = world.getBodyList(); b != null; b = b.m_next)
                    {
                        if(b.getParts().length > 0)
                        {
                            Part p = b.getParts()[0];
                            if (p != null && !b.m_isFixed && !b.isSleeping() && !p.isCharacter) {
                                outputbuf.putShort((short) p.getID());
                                outputbuf.putShort((short)b.hash());
                                writeVec2D(new Vec2D(b.getPosition()));
                                outputbuf.putFloat(b.getAngle());
                            }
                        }
                    }
                outputbuf.putShort(Short.MIN_VALUE);
                writeData(outputbuf);
                }
            }
        });
    }

    public void sendCharacterPositions()
    {
        addTask(new Runnable()
        {
            public void run() {
                Player player = connection.getPlayer();
                World world = null;
                try {
                    if (player != null && player.getProp("Character") != null && player.getProp("Character") instanceof net.worlds.object.Character) {
                        world = player.getObjectProp("Character").world;
                    }
                } catch (WorldsException ex) {
                    ex.printStackTrace();
                }
                if(world != null)
                {
                    outputbuf.clear();
                    outputbuf.put((byte) 7);
                    writeString(world.name);
                    synchronized(world.getObjectsByType(Box.class))
                    {
                        for(GameObject obj : world.getParts())
                        {
                            try {
                                final Part p = (Part) obj;
                                if (p != null && p.isCharacter && p.getBody() != null && connection != null && connection.getPlayer() != null && p.getParent() instanceof net.worlds.object.Character && p.getParent() != connection.getPlayer().getProp("Character") && !p.getBooleanProp("Fixed") && !p.getBody().isSleeping() && p.getBody() != null && p.getBody().m_actualPosition != null) {
                                    outputbuf.putShort((short) p.getID());
                                    outputbuf.putShort((short)p.getBody().hash());
                                    writeVec2D(p.getBody().m_actualPosition);
                                    outputbuf.putFloat(p.getBody().getAngle());
                                }
                            } catch (WorldsException ex) {
                                ex.printStackTrace();
                            }
                        }
                    }
                    outputbuf.putShort(Short.MIN_VALUE);
                    writeData(outputbuf);
                    synchronized(world.getObjectsByType(net.worlds.object.Character.class))
                    {
                        for(GameObject obj : world.getObjectsByType(net.worlds.object.Character.class))
                        {
                            sendArmRotation((net.worlds.object.Character)obj, ((net.worlds.object.Character)obj).net_armAngle);
                        }
                    }
                }
            }
        });
    }

    public void sendObjectVelocities()
    {
        addTask(new Runnable()
        {
            public void run() {
                Player player = connection.getPlayer();
                World world = null;
                try {
                    if (player != null && player.getProp("Character") != null && player.getProp("Character") instanceof net.worlds.object.Character) {
                        world = player.getObjectProp("Character").world;
                    }
                } catch (WorldsException ex) {
                    ex.printStackTrace();
                }
                if(world != null)
                {
                    outputbuf.clear();
                    outputbuf.put((byte) 8);
                    writeString(world.name);
                    for(Body b = world.getBodyList(); b != null; b = b.m_next)
                    {
                        if(b.getParts().length > 0)
                        {
                            Part p = b.getParts()[0];
                            if (p != null && !b.m_isFixed && !b.isSleeping() && !p.isCharacter) {
                                outputbuf.putShort((short) p.getID());
                                outputbuf.putShort((short)b.hash());
                                writeVec2D(new Vec2D(b.getLinearVelocity()));
                                outputbuf.putFloat(b.getAngularVelocity());
                            }
                        }
                    }
                    outputbuf.putShort(Short.MIN_VALUE);
                    writeData(outputbuf);
                }
            }
        });
    }
    public void sendPlayer(final Player p)
    {
        
        {
            addTask(new Runnable()
            {
                public void run() {
                    outputbuf.clear();
                    outputbuf.put((byte)10);
                    outputbuf.putShort((short) p.getID());
                    writeData(outputbuf);
                }
            });
        }
    }
    public void sendWorldStatus(final int status)
    {
        
        {
            addTask(new Runnable()
            {
                public void run() {
                    outputbuf.clear();
                    outputbuf.put((byte) 13);
                    outputbuf.put((byte) status);
                    writeData(outputbuf);
                }
            });
        }
    }
    public void sendInUse(final GameObject obj, final boolean status)
    {
        
        {
            addTask(new Runnable()
            {
                public void run() {
                    outputbuf.clear();
                    outputbuf.put((byte)15);
                    writeObject(obj);
                    writeBoolean(status);
                    writeData(outputbuf);
                }
            });
        }
    }
    public void sendConsoleOutput(final String text)
    {
        addTask(new Runnable()
        {
            public void run() {
                outputbuf.clear();
                outputbuf.put((byte)16);
                writeString(text);
                writeData(outputbuf);
            }
        });
    }
    public void sendCursorPosition(final RemotePlayer p, final Vec2D pos)
    {
        if(p.getConnection().getPlayer() != null)
        {
            addTask(new Runnable()
            {
                public void run() {
                    outputbuf.clear();
                    outputbuf.put((byte)17);
                    outputbuf.putShort((short) p.getConnection().getPlayer().getID());
                    writeVec2D(pos);
                    writeData(outputbuf);
                }
            });
        }
    }
    public void sendIDChange(final int oldID, final int newID)
    {    
        addTask(new Runnable()
        {
            public void run() {
                outputbuf.clear();
                outputbuf.put((byte)19);
                outputbuf.putShort((short)oldID);
                outputbuf.putShort((short)newID);
                writeData(outputbuf);
            }
        });
    }
    public void sendObjectFinish(final Part p)
    {
        addTask(new Runnable()
        {
            public void run() {
                outputbuf.clear();
                outputbuf.put((byte)20);
                outputbuf.putShort((short)p.getID());
                writeData(outputbuf);
            }
        });
    }
    public void sendChat(final Color c, final String msg)
    {
        addTask(new Runnable()
        {
            public void run() {
                outputbuf.clear();
                outputbuf.put((byte)21);
                writeColor(c);
                writeString(msg);
                writeData(outputbuf);
            }
        });
    }
    public void sendMoveTo(final Part p, final int action)
    {
        addTask(new Runnable()
        {
            public void run() {
                outputbuf.clear();
                outputbuf.put((byte)22);
                outputbuf.putShort((short)p.getID());
                outputbuf.put((byte)action);
                writeData(outputbuf);
            }
        });
    }
    public void sendMessage(final int type, final String text)
    {
        addTask(new Runnable()
        {
            public void run() {
                outputbuf.clear();
                outputbuf.put((byte)23);
                outputbuf.put((byte)type);
                writeString(text);
                writeData(outputbuf);
            }
        });
    }
    public void sendSelectedTool(final net.worlds.object.Character c, final Tool t)
    {
        addTask(new Runnable()
        {
            public void run() {
                outputbuf.clear();
                outputbuf.put((byte)27);
                outputbuf.putShort((short)c.getID());
                if(t == null)
                    outputbuf.putShort((short) -1);
                else
                    outputbuf.putShort((short) t.getID());
                writeData(outputbuf);
            }
        });
    }
    public void sendArmRotation(final net.worlds.object.Character c, final double rotation)
    {
        outputbuf.clear();
        outputbuf.put((byte)28);
        outputbuf.putShort((short)c.getID());
        outputbuf.putFloat((float)rotation);
        writeData(outputbuf);
    }
    public void sendKick(final String message)
    {
        addTask(new Runnable()
        {
            public void run() {
                outputbuf.clear();
                outputbuf.put((byte)24);
                writeString(message);
                writeData(outputbuf);
            }
        });
    }
    public void sendSoundPlay(final Sound s)
    {
        addTask(new Runnable()
        {
            public void run() {
                outputbuf.clear();
                outputbuf.put((byte)31);
                outputbuf.putShort((short)s.getID());
                writeData(outputbuf);
            }
        });
    }
    public void sendSoundStop(final Sound s)
    {
        addTask(new Runnable()
        {
            public void run() {
                outputbuf.clear();
                outputbuf.put((byte)32);
                outputbuf.putShort((short)s.getID());
                writeData(outputbuf);
            }
        });
    }
    private void writeBoolean(boolean v)
    {
        if(v)
            outputbuf.put((byte)1);
        else
            outputbuf.put((byte)0);
    }
    private boolean getBoolean()
    {
        if(inputbuf.get() == 1)
            return true;
        else
            return false;
    }
    private void writeLongString(String s)
    {
        outputbuf.putInt((short) s.length());
        outputbuf.put(s.getBytes());
    }
    private void writeString(String s)
    {
        outputbuf.putShort((short) s.length());
        outputbuf.put(s.getBytes());
    }
    private String getString()
    {
        int len = inputbuf.getShort();
        byte[] bytes = new byte[len];
        for(int i = 0; i < len; i++)
            bytes[i] = inputbuf.get();
        return new String(bytes);
    }
    private void writeVec2D(Vec2D v)
    {
        outputbuf.putFloat((float) v.x);
        outputbuf.putFloat((float) v.y);
    }
    private Vec2D getVec2D()
    {
        return new Vec2D(inputbuf.getFloat(), inputbuf.getFloat());
    }
    private void writeColor(Color c)
    {
        outputbuf.put((byte) c.getRed());
        outputbuf.put((byte) c.getGreen());
        outputbuf.put((byte) c.getBlue());
    }
    private Color getColor()
    {
        int r = Util.parseUnsignedByte(inputbuf.get());
        int g = Util.parseUnsignedByte(inputbuf.get());
        int b = Util.parseUnsignedByte(inputbuf.get());
        return new Color(r, g, b);
    }
    private void writeObject(GameObject obj)
    {
        if(obj == null)
            outputbuf.putShort(Short.MIN_VALUE);
        else
            outputbuf.putShort((short) obj.getID());
    }
    private GameObject getObject(int id)
    {
        Util.checkThread();
        if(id == Short.MIN_VALUE)
            return null;
        else
            return Main.getGame().getByID(id);
    }

    private class PingThread implements Runnable
    {

        public void run() {
            while(true)
            {
                if(!running)
                    return;
                sendPing();
                try {
                    Thread.sleep(30000);
                } catch (InterruptedException ex) {
                }
            }
        }
    }
    private class PositionUpdateThread implements Runnable
    {
        public void run() {
            while(true)
            {
                if(!running)
                    return;
                try {
                    sendObjectPositions();
                    Thread.sleep(50);
                    sendCharacterPositions();
                    Thread.sleep(50);
                } catch (InterruptedException ex) {
                }
            }
        }
    }
    private class CharacterUpdateThread implements Runnable
    {
        public void run() {
            while(true)
            {
                if(!running)
                    return;
                    sendCharacterPositions();
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ex) {
                }
            }
        }
    }
    private class VelocityUpdateThread implements Runnable
    {
        public void run() {
            while(true)
            {
                if(!running)
                    return;
                sendObjectVelocities();
                try {
                    Thread.sleep(200);
                } catch (InterruptedException ex) {
                }
            }
        }
    }
    private class PlayerUpdateThread implements Runnable
    {

        public void run() {
            while(true)
            {
                for(final PlayerCursor c : Main.getServer().cursors.values())
                {
                    Main.runInMainThread(new Runnable()
                    {
                        public void run() {
                            sendCursorPosition(c.client, c.pos);
                        }
                    });
                }
                try {
                    Thread.sleep(100);
                } catch (InterruptedException ex) {
                    ex.printStackTrace();
                }
            }
        }

    }
}
